/*
 * Copyright (c) 2021-2021. Gardel
 *
 * This file is part of Source2Pdf (https://gitee.com/gardel/source2pdf).
 *
 * Source2Pdf is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Source2Pdf is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Source2Pdf.  If not, see <http://www.gnu.org/licenses/>.
 */

package cn.sunxinao.source2pdf.utils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Window;

public class EventUtils {
    private static final Method windowAddEventMethod;
    private static final Method sceneAddEventMethod;
    private static final Method nodeAddEventMethod;
    private static final Method windowRemoveEventMethod;
    private static final Method sceneRemoveEventMethod;
    private static final Method nodeRemoveEventMethod;

    static {
        try {
            windowAddEventMethod = Window.class.getMethod("addEventHandler", EventType.class, EventHandler.class);
            sceneAddEventMethod = Scene.class.getMethod("addEventHandler", EventType.class, EventHandler.class);
            nodeAddEventMethod = Node.class.getMethod("addEventHandler", EventType.class, EventHandler.class);
            windowRemoveEventMethod = Window.class.getMethod("removeEventHandler", EventType.class, EventHandler.class);
            sceneRemoveEventMethod = Scene.class.getMethod("removeEventHandler", EventType.class, EventHandler.class);
            nodeRemoveEventMethod = Node.class.getMethod("removeEventHandler", EventType.class, EventHandler.class);
        } catch (NoSuchMethodException e) {
            throw new IllegalStateException(e);
        }
    }

    private EventUtils() {
        throw new IllegalStateException();
    }

    public static <T extends Event> void addListenerDeeply(Node node, Iterable<EventType<T>> allTypes, EventHandler<T> listener) {
        addListenerCombine(node, allTypes, listener);
        if (node instanceof Parent parent) {
            ObservableList<Node> children = parent.getChildrenUnmodifiable();
            for (Node child : children) {
                addListenerDeeply(child, allTypes, listener);
            }
            children.addListener((ListChangeListener<Node>) c -> {
                while (c.next()) {
                    if (c.wasAdded()) {
                        for (Node newChild : c.getAddedSubList()) {
                            addListenerDeeply(newChild, allTypes, listener);
                        }
                    }
                }
            });
        }
    }

    public static <T extends Event> void addListenerCombine(EventTarget eventTarget, Iterable<EventType<T>> allTypes, EventHandler<T> listener) {
        Method addMethod;
        Method removeMethod;
        if (eventTarget instanceof Window) {
            addMethod = windowAddEventMethod;
            removeMethod = windowRemoveEventMethod;
        } else if (eventTarget instanceof Scene) {
            addMethod = sceneAddEventMethod;
            removeMethod = sceneRemoveEventMethod;
        } else if (eventTarget instanceof Node) {
            addMethod = nodeAddEventMethod;
            removeMethod = nodeRemoveEventMethod;
        } else {
            return;
        }
        try {
            for (EventType<?> eventType : allTypes) {
                removeMethod.invoke(eventTarget, eventType, listener);
                addMethod.invoke(eventTarget, eventType, listener);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            throw (RuntimeException) e.getTargetException();
        }
    }
}
